#!/usr/bin/env python
##
## This file is part of the sigrok-util project.
##
## Copyright (C) 2018 Gerhard Sittig <gerhard.sittig@gmx.net>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <http://www.gnu.org/licenses/>.
##

"""
Disguise EEVblog 121GW Bluetooth communication as a COM port (devel HACK).

This script runs as an external support process, or optionally can run on
a dedicated machine, to relay the meter's BT communication to a COM port,
where sigrok's serial-dmm driver can access the multimeter's acquisition
data. The sigrok application then gets invoked like so:

  $ sigrok-cli -d eevblog-121gw:conn=/dev/ttyUSB0 --continuous

This approach is a development HACK until the sigrok project may receive
better integration of BLE communication. But it also helps abstract away
platform dependent details or the choice for BLE communication libraries
or tools, and helps speed up the development of the 121GW meter device
support. Another benefit is the separation of the BLE communication from
the packet inspection and data processing (priviledges are required for
the former but not for the latter, COM ports typically are accessible to
local users already).

A future approach might perhaps support something like:

  -d eevblog-121gw:conn=ble/121gw/<bdaddr>
  -d eevblog-121gw:conn=tcp-ser/<ip>/<port>
"""

import argparse
from bluepy import btle
import serial
import sys
import time

_ble_scan_dev = "hci0"
_ble_scan_time = 1
_ble_scan_desc = "Complete Local Name"
_ble_scan_value = "121GW"

_ble_write_handle = 9
_ble_write_data = [ 0x03, 0x00, ]
_ble_read_handle = 8

# Verbosity levels:
# - 0, quiet operation
# - 1 (default), "summary" (got/lost connection, termination)
# - 2, plus serial data
# - 3, plus BT scan/comm details
_verbose = None

def _diag_print(level, text):
	if level > _verbose:
		return
	print("DIAG: {}".format(text))

def _parse_cmdline():
	parser = argparse.ArgumentParser(
		description = "EEVblog 121GW Bluetooth to serial converter"
	)
	parser.add_argument(
		"-i", "--hcidev",
		help = "BT HCI device name",
		default = _ble_scan_dev
	)
	parser.add_argument(
		"-b", "--bdaddr",
		help = "BT device address",
		default = ""
	)
	parser.add_argument(
		"-p", "--comport",
		help = "COM port to send bytes to",
		default = ""
	)
	parser.add_argument(
		"-B", "--bitrate",
		help = "bit rate for COM port data",
		default = 115200
	)
	parser.add_argument(
		"-v", "--verbose",
		help = "increase verbosity",
		action = "count", default = 1
	)
	parser.add_argument(
		"-q", "--quiet",
		help = "decrease verbosity",
		action = "count", default = 0
	)
	args = parser.parse_args()
	return args

def _open_comport(args):
	if not args:
		return None
	if not args.comport:
		return None
	com_port = serial.Serial(
		args.comport, args.bitrate,
		bytesize = serial.EIGHTBITS,
		parity = serial.PARITY_NONE,
		stopbits = serial.STOPBITS_ONE)
	return com_port

def _send_comport(port, data):
	port.write(data)

class CommDelegate(btle.DefaultDelegate):

	def __init__(self, port):
		btle.DefaultDelegate.__init__(self)
		self._port = port

	def handleNotification(self, handle, data):
		if handle != _ble_read_handle:
			return
		data = bytearray(data)
		self.parse_data(data)

	def parse_data(self, data):
		text = " ".join([ "{:02x}".format(d) for d in data ])
		_diag_print(2, "got BLE data: {}".format(text))
		_send_comport(self._port, data)

def _scan_bleaddr(args):
	# If not specified by the user, Scan for the device's BDADDR.

	if not args:
		return None
	if args.bdaddr:
		return args.bdaddr

	iface = args.hcidev
	if iface.startswith("hci"):
		iface = iface.replace("hci", "")
	iface = int(iface)

	_diag_print(3, "Scanning (iface {}) ...".format(iface))
	scanner = btle.Scanner(iface)
	try:
		devices = scanner.scan(_ble_scan_time)
	except btle.BTLEException as e:
		return None
	except KeyboardInterrupt as e:
		return None
	bdaddr = False
	for dev in devices:
		a, t, sd = dev.addr, dev.addrType, dev.getScanData()
		for adtype, desc, value in sd:
			if desc != _ble_scan_desc:
				continue
			if value != _ble_scan_value:
				continue
			bdaddr = a
			break

	_diag_print(3, "Done scanning, addr {}".format(bdaddr))
	return bdaddr

def _open_bleconn(args, port):
	bdaddr = _scan_bleaddr(args)
	if not bdaddr:
		return False

	_diag_print(3, "Opening bluepy(3) connection ...")
	try:
		conn = btle.Peripheral(bdaddr, btle.ADDR_TYPE_PUBLIC)
	except btle.BTLEException as e:
		return False
	except KeyboardInterrupt as e:
		return None
	conn.setDelegate(CommDelegate(port))
	data = bytearray(_ble_write_data)
	conn.writeCharacteristic(_ble_write_handle, data)
	_diag_print(3, "bluepy(3) connection {}".format(conn))
	return conn

def main():
	global _verbose

	args = _parse_cmdline()
	_verbose = args.verbose - args.quiet

	com_port = _open_comport(args)
	if not com_port:
		sys.exit(1)

	conn = None
	while True:
		# Automatically (re-)create connections, handle absence
		# of devices (retry later).
		if not conn:
			_diag_print(3, "connecting to BT ...")
			conn = _open_bleconn(args, com_port)
			if conn is None:
				break
			if conn:
				_diag_print(1, "got BLE connection.")
		if not conn:
			_diag_print(3, "could not connect to BT, waiting ...")
			time.sleep(.5)
			continue
		# Wait for incoming notifications. A registered handler
		# will process the data. Gracefully handle lost connections.
		try:
			conn.waitForNotifications(1.0)
		except KeyboardInterrupt as e:
			_diag_print(1, "got CTRL-C, terminating ...")
			break
		# except btle.BTLEException.DISCONNECTED as e:
		except btle.BTLEException as e:
			_diag_print(1, "lost BLE connection.")
			conn = None

	return

if __name__ == "__main__":
	main()
